TS-05 高级类型
交叉类型
使用&符号定义交叉类型,也可以叫做与的操作
下面的例子组合两个对象,当组合两个对象时,就可以使用&的操作
const mergeFunc = <T, U>(arg1: T, arg2: U): T & U => {
let res = {} as T & U // 使用类型断言
res = Object.assign(arg1, arg2)
return res
}
mergeFunc({a: 'a'}, {b: 'b'})
联合类型
使用|符号定义联合类型
type1 | type2 | type3
即可以是type1
也可以是type2
或者type3
类型保护
const valueList = [123, 'abc']
const getRandomValue = () => {
const number = Math.random() * 10
if(number < 5) return valueList[0]
else return valueList[1]
}
const val = getRandomValue()
// 由于以上会返回两种类型,因此下面的判断会出错,需要使用类型断言
if((val as string).length) {
console.log((val as string).length)
} else {
console.log((val as number).toFixed())
}
上面的例子中,使用类型断言太多,比较麻烦,因此需要类型保护
第一种类型保护: 使用类型谓词,谓词为
parameterName is Type
这种形式ts// 定义一个函数 function isString(value: string | number): value is string { return typeof value === 'string' } // 之后再进行判断 if(isString(item)){ // 如果为true则该代码块的类型都是string类型的 console.log(item.length) } else { console.log(item.toFixed()) }
第二种类型保护:
typeof
tsif(typeof item === 'string'){ console.log(item.length) } else { console.log(item.toFixed()) }
typeof在ts中作为类型保护,使用需要注意以下几点:
- 只能是全等或者全不等操作符,不能使用
includes
来,如:(typeof item).includes('string')
是错误的 - 只能是以下四种中的一种:
string
、number
、boolean
、symbol
- 只能是全等或者全不等操作符,不能使用
第三种类型保护,
instanceof
用与检测是否是某个类的实例
可以为null的类型
TS具有两种特殊类型:null
和undefined
,默认情况下,类型检查器会自动赋值null
或者undefined
,当开启--strictNullChecks
标记时,当声明变量,不会再自动赋值null
或者undefined
,需要手动赋值了;
let s = 'foo'
s = undefined // 报错
let sn: string | null = 'foot'
sn = null
sn = undefined // 报错, 没有undefined
注意: TypeScript会把null
和undefined
区别对待,string | null
, string | undefined
, string | null | undefined
三种是不同的类型
类型断言
使用!来进行类型断言
使用!
断言的情况为undefined
的情况,声明一个值不赋初始值
const getLength = (value: string | null): string {
function getRes(prefix: string) {
// 这里使用!来类型断言,因为num可能为undefined
return prefix + num!.toFixed().toString()
}
num = num || 0.1
return getRes(num)
}
类型别名
类型别名只是为类型起了一个名字,相当于js中的赋值
type TypeString = string
let str: TypeString = 'str' // 可以直接使用类型别名了
对象别名只能在属性中引用自身,不能在其他地方使用自身
// 嵌套例子, 在对象中引用自身
type Childs<T> {
current: T
child?: Childs<T>
}
// 错误引用例子
type Childs = Childs[]
注意: 当接口定义别名时,不能使用extends
和implements
,因为别名是没有这两个属性的,只有接口有
当无法通过接口并且需要使用联合类型或者元组类型时,用类型别名,当需要使用extends
和implements
时使用接口
字面量类型
- 字符串字面量
type Name = 'Heny'
let str: Name = 'Heny' // 只能是这一种类型
type Direction = 'north' | 'east' | 'south' | 'west'
function getDirectionFirstLetter(direction: Direction){
return direction.substr(0, 1)
}
getDirectionFirstLetter('north') // 只能是四个中的其中一个,ts会自动提示有哪些,交互效果很好
- 数字字面量和字符串字面量同理;
可辨识联合
单例类型:指的是枚举成员类型或者字面量类型
我们可以把单例类型、联合类型、类型保护、类型别名这几种类型进行合并,来创建可辨识联合类型的高级模式,也可以称作标签联合和代数数据类型
三要素:
- 具有普通的单例类型属性
- 一个类型别名包含了哪些类型的联合
- 此属性上的类型保护
interface Square{
kind: 'square'
size: number
}
interface Rectangle {
kind: 'rectangle'
height: number
width: number
}
interface Circle {
kind: 'circle'
radius: number
}
type Shape = Square | Rectangle | Circle
function getArea(s: Shape) {
switch(s.kind) {
case 'square': return s.size * s.size
case 'rectangle': return s.height * s.width
case 'circle': return Math.PI * s.radius ** 2 // **表示平方
}
}
上面的例子中,三个接口的kind
就是一个单例类型属性,称为可辨识的属性或者标签,类型别名Shape包含了三个类型的联合,当走到switch代码里面时,会自动将可以访问的属性都列举出来
- switch遗漏判断条件处理方式
// 第一种,开启--strictNullChecks,并且指定getArea返回值类型
// 第二种定义一个错误函数
function asserNever(value: never): never => {
throw new Error('Unexpected object: ' + value)
}
function getArea(s: Shape): number {
switch(s.kind) {
case 'square': return s.size * s.size
case 'rectangle': return s.height * s.width
case 'circle': return Math.PI * s.radius ** 2 // **表示平方
default: return asserNever(s) // 如果前面再遗漏, 则会报错提示
}
}
多态的this 类型
多态的this
类型表示的是某个包含类或接口的子类型,它能很容易的表现连贯接口间的继承,比如在计算器的例子中,在每个操作之后都返回this
类型:
class Counter {
constructor (public count: number = 0){}
public add(value: number){
this.count += value
return this
}
public substract(value: number) {
this.count -= value
return this
}
}
let counter: Counter = new Counter(5)
class PowCounter extends Counter {
constructor(public count: number = 0) {
super(count)
}
public pow(value: number) {
this.count = this.count ** value
return this
}
}
let p: PowCounter = new PowCounter(5)
console.log(p.add(5).substract(2).pow(3))
索引类型
使用keyof
连接符,连接一个类型,之后返回由这个类型的所有属性名
组成的联合类型
interface Info {
name: string;
age: number
}
let info: keyof Info; // info的类型将变成(name | age)类型;
函数使用keyof
连接符:
function getValue<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n])
}
const infoObj = {
name: 'hh',
age: 18
}
getValue(infoObj, ['name', 'age'])
索引访问类型
和访问对象的某个属性值是一样的,在ts中用来访问某个属性的类型,用在接口添加keyof
时,会返回接口所有的类型
组成的联合类型
// 例子一
interface Info2 {
name: string;
age: number
}
type NameT = Info2['name'] // NameT为string类型
// 例子二
interface Type {
a: never;
b: string;
c: number;
d: undefined;
e: null;
f: object
}
type Test = Type[keyof Type] // keyof Type将是非never,undefined,null的其他类型组成的联合类型
使用[keyof T]
访问的是接口类型中不为undefined
、null
、never
类型组成的联合类型
映射类型
使用in
连接符,in
相当于for in
遍历操作符;
当需要给一个接口里面的所有属性添加readonly
修饰符时,可以使用in keyof
操作符
interface Info {
name: string;
age: number;
size: number
}
type ReadInfo<T> = {
// P相当于是T的每一个属性名,T就是接口,下面可以添加可选操作符或者readonly修饰符
[P in keyof T]: T[P]
}
type ReadInfo1 = ReadInfo<Info>
// ReadInfo的类型即是Info里面的所有属性修改为只读的可选三个属性了;
在TypeScript
中,给接口的所有属性添加readonly
修饰符和可选的修改符,一般是常用的,TypeScript
提供了两个内置映射类型,可以直接拿来使用;
Readonly
只读映射 和 Partial
可选映射,这两个都是经常使用的映射类型;
基本实现:
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
type Partial<T> = {
[P in keyof T]?: T[P]
}
基本使用:
type ReadInfo = Readonly<Info> // 使用前面定义的Info接口
type PartialInfo = Partial<Info>
最简单的映射类型和它的组成成分
type Keys = 'option1' | 'option2'
type Flags = {[K in Keys]: boolean;}
它的语法与索引签名的语法类型,内部使用了for...in
,具有三个部分:
- 类型变量
K
,它会依次绑定到每个属性。 - 字符串字面量联合的
Keys
,它包含了要迭代的属性名的集合 - 属性的结果类型
在个简单的例子里, Keys
是硬编码的的属性名列表并且属性类型永远是 boolean
,因此这个映射类型等同于:
type Flags = {
option1: boolean;
option2: boolean;
}
在真正的应用里,可能不同于上面的 Readonly
或 Partial
。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是 keyof
和索引访问类型要做的事情:
type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }
但它更有用的地方是可以有一些通用版本
type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }
还有两个内置映射类型:Pick
、Record
// Pick的实现,Pick指的是原来的对象里面的一部分属性名组成的一个类型
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
// Record的实现 Record适用与将一个对象中的每一个属性转换为其他值的一个类型
type Record<K extends keyof any, T> = {
[P in K]: T;
}
Pick
的使用场景:
interface Info {
name: string;
age: number;
address: string;
}
const info: Info = {
name: 'hh';
age: 18;
address: 'beijing'
}
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>{
const res: any = {}
keys.map(key => {
res[key] = obj[key]
})
return res
}
const nameAddress = pick(info, ['name', 'address'])
// nameAddress的类型为:Pick<Info, 'name' | 'address'>
Record
使用场景:常用于要把对象中的属性转换为其他值的一个场景
function mapObject<K extends keyof string | number, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>{
let res: any = {}
for(const key in obj) {
res[key] = f(obj[key])
}
return res
}
const names = {0: 'hello', 1: 'wrod', 2: 'bye'}
const lengths = mapObject(names, s => s.length)
Record
简单使用:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
同态:两个相同类型的代数结构保持映射;
Readonly
、Partial
、Pick
都是同态的;Record
不是同态,因为Record
并不需要输入类型来拷贝属性,所以它不属于同态
非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。
映射类型进行推断
- 类型包装
T[P]
被包装在Proxy<T>
的类里
type Proxy<T> = {
get(): T,
// set接收的值的类型必须和get返回值的类型相同
set(val: T): void
}
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>
}
function proxify<T>(obj: T): Proxify<T>{
const result = {} as Proxify<T>
for(const key in obj){
result[key] = {
get: () => obj[key],
set: (val) => obj[key] = val
}
}
return result
}
let props = {
name: 'heny',
age: 18
}
let proxyProps = proxify(props)
- 类型拆包
function unproxify<T>(t: Proxify<T>): T{
const result = {} as T
for(const k in t) {
result[k] = t[k].get()
}
return result
}
let originalProps = unproxify(proxyProps)
先看下怎么封装的,之后写个逆向的拆包,这个就叫做使用映射类型进行推断原始类型;
注意:这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。
增加或移除特定修饰符
使用+
为增加修饰符,使用-
为移除特定修饰符
type ReadonlyInfo<T> = {
+readonly [P in keyof T]-?: T[P]
}
条件类型
在ts2.8
以后的类型,它会进行一个条件表达式,之后在其中的一个类型中选择一个类型,简单例子:T extends U ? X : Y
type Type1<T> = T extends string ? string : nubmer
分布式条件类型
当待检测的类型是一个联合类型的时候,该条件类型就被称为分布式条件类型,在实例化的时候,ts会自动的分化成联合类型;
type TypeName<T> =
T extends string ? string :
T extends number ? number :
T extends boolean ? boolean :
T extends undefined ? undefined :
T extends () => void ? () => void :
object
type Type1 = TypeName<() => void> // 返回函数类型
type Type2 = TypeName<string[]> // 返回object类型
type Type3 = TypeName<(() => void) | string[]> // 返回联合类型
例子二:
type Diff<T, U> = T extends U ? never : T
// 判断T的类型是否是U的子类型,经过判断,number是,因此只有number没有被返回回来
type Type2 = Diff<string | number | boolean, undefined | number>
// Type2的类型为 string | boolean
例子三:
type Type3<T> = {
[P in keyof T]: T[P] extends Function ? P : never
}[keyof T] // 索引访问类型,将所有的类型组成联合类型
interface Part {
id: number;
name: string;
subParts: Part[];
undatePart(name: string): void;
subAdd(val: number): number;
}
type Type4 = Type3<Part>
// Types4的类型为 undatePart | subAdd,因为只有这两个是函数类型
条件类型的类型推断
使用infer
关键字用来推断类型;
例子:当传入的是数组时,返回数组里面的元素的类型,如果传入的不是数组,则直接返回该类型
// 不使用infer实现时,这里的T[number]为索引访问
type Type8<T> = T extends any[] ? T[number] : T
type Type9 = Type8<string[]> // 返回string类型,
type Type10 = Type8<string> //string
// 使用infer实现,利用一个新泛型变量,直接使用U和上面同理
type Type11<T> = T extends Array<infer U> ? U : T
预定义的有条件类型
Exclude<T, U>
-- 从T
中剔除可以赋值给U
的类型。Extract<T, U>
-- 提取T
中可以赋值给U
的类型。NonNullable<T>
-- 从T
中剔除null
和undefined
。ReturnType<T>
-- 获取函数返回值类型。InstanceType<T>
-- 获取构造函数类型的实例类型。
type Type1 = Exclude<'a' | 'b' | 'c', 'b' | 'c'> // Type1类型为'a'
type T03 = Extract<string | number | (() => void), Function>; // () => void
type T10 = ReturnType<() => string>; // string
type T11 = ReturnType<(s: string) => void>; // void
function f1(s: string) {
return { a: 1, b: s };
}
type T14 = ReturnType<typeof f1>; // { a: number, b: string }
class C {
x = 0;
y = 0;
}
type T20 = InstanceType<typeof C>; // C
type T21 = InstanceType<any>; // any
type T22 = InstanceType<never>; // any
type T23 = InstanceType<string>; // Error
type T24 = InstanceType<Function>; // Error
// 构造函数的默认参数写法
function fn(new (...args: any[]) => any) // 传入的就是一个构造函数